/**
* Copyright (c) 2010, Attila Magyar
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the Attila Magyar nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.uftp.net.channels;

import java.io.IOException;
import java.io.Writer;
import java.util.Vector;
import org.uftp.FTPMode;
import org.uftp.FTPNotConnectedException;
import org.uftp.FTPSocketException;
import org.uftp.InvalidReplyException;
import org.uftp.buffers.BufferFactory;
import org.uftp.monitors.FTPMessageListener;
import org.uftp.FTPReply;
import org.uftp.buffers.ReadBuffer;
import org.uftp.log.FTPLogManager;
import org.uftp.log.FTPLogger;
import org.uftp.net.IFTPClientSocket;
import org.uftp.net.IFTPServerSocket;
import org.uftp.net.SocketManagerFactory;
import org.uftp.parsers.ReplyParser;

public class ControlChannel {
    
    private static FTPLogger logger = FTPLogManager.getDefaultLogger( ControlChannel.class.getName() );
    
    private static final String CLRF = "\r\n";
    
    private Vector msgListeners;
    private IFTPClientSocket socket;
    private String remoteHost;
    private int remotePort;
    private int[] bindIp = new int[] { 127, 0, 0, 1 };
    private Writer writerStream = null;
    private ReadBuffer readerStream = null;
    private ReplyParser rparser;    
    private int portRangeBegin;
    private int portRangeEnd;
    
    public ControlChannel( String remoteHost, int remotePort ) {
        this();
        this.remoteHost = remoteHost;
        this.remotePort = remotePort;
        init();
    }   
    
    public ControlChannel() {
        msgListeners = new Vector();
        rparser = new ReplyParser();
    }   
    
    public void init() {
        socket = 
                SocketManagerFactory.getDefaultManager().createClientSocket( remoteHost, remotePort );        
    }
    
    public void connect() throws FTPSocketException {
        socket.connect();   
        
        readerStream = BufferFactory.createDefaultReadBuffer( socket.getInputStream() );                
        writerStream = BufferFactory.newStreamWriter( socket.getOutputStream() );
    }

    public void close() throws FTPSocketException {
        if ( isConnected() ) {
            socket.close();
        }            
    }    
    
    public boolean isConnected() {
        if ( socket == null || readerStream == null || writerStream == null ) return false;
        return socket.isConnected();
    }
    
    public void checkConnection() {
        if ( !isConnected() ) {
            throw new FTPNotConnectedException( "Not connected to " + remoteHost + ":" + remotePort );
        }
    }
    
    public FTPReply sendCommand(String command ) throws FTPSocketException, InvalidReplyException {
        
        checkConnection();
        
        try {
            
            logCommand( command );

            writerStream.write( command );
            writerStream.write( CLRF );            
            writerStream.flush();
        
            return readReply();
            
        } catch (IOException ex) {
            throw new FTPSocketException( ex );
        }
    }
    
    public FTPReply sendAndValidateCommand(String command, int code) throws FTPSocketException, InvalidReplyException {
        return sendAndValidateCommand( command, new int[]{code} );
    }
    
    public FTPReply sendAndValidateCommand(String command, int[] codes) throws FTPSocketException, InvalidReplyException {
        FTPReply rp = sendCommand( command );
        rp.validate( codes );
        return rp;
    }
    
    public FTPReply readReply() throws FTPSocketException, InvalidReplyException {
        
        checkConnection();
        
        try {
            
            String rp = readerStream.readLine();                                                                        
            while ( rp != null && rp.length() == 0 ) {
                rp = readerStream.readLine();
            }            
            
            if ( rp == null ) {
                throw new InvalidReplyException( "Null reply received" );
            }            
            if ( rp.length() < 3 ) {
                throw new InvalidReplyException( "Reply is too short" );
            }            
            
//         Thus the format for multi-line replies is that the first line
//         will begin with the exact required reply code, followed
//         immediately by a Hyphen, "-" (also known as Minus), followed by
//         text.  The last line will begin with the same code, followed
//         immediately by Space <SP>, optionally some text, and the Telnet
//         end-of-line code.
//
//            For example:
//                                123-First line
//                                Second line
//                                  234 A line beginning with numbers
//                                123 The last line            
//            
//         The user-process then simply needs to search for the second
//         occurrence of the same reply code, followed by <SP> (Space), at
//         the beginning of a line, and ignore all intermediary lines.  If
//         an intermediary line begins with a 3-digit number, the Server
//         must pad the front  to avoid confusion.            
//         ...            
//         RFC 959             
                 
            int code = rparser.parse( rp ).getCode();
            
            if ( rp.charAt(3) == '-' ) {    // multiline
                                                 
                 Vector lines = new Vector();
                 boolean complete = false;
                 lines.addElement( rp.substring(3) );
                 
                 while (!complete) {
                     rp = readerStream.readLine();                    
                        
                     if ( rp == null ) {
                        throw new InvalidReplyException( "Null reply received" );
                    }            

                    if ( rp.length() != 0 ) {
                                              
                        lines.addElement( rp.substring( 3 ) );
                        
                        // end of multiline
                        if (rp.length() > 3 && 
                                rp.substring(0, 3).equals( Integer.toString(code) ) && 
                                rp.charAt(3) == ' ') {
                            
                             complete = true;
                        }
                        
                    }
                 }                 
                 
                 FTPReply frep = new FTPReply( lines, code );
                 logReply( frep.toString() );
                 return frep;
            }
            else {
                // single line
                logReply( rp );
                return rparser.parse( rp );            
            }
        }
        catch ( IOException e ) {
            throw new FTPSocketException( e );
        }
        
    }    
    
    public DataChannel createDataConnection(int mode) throws FTPSocketException, InvalidReplyException {
        
        checkConnection();
        
        if ( mode == FTPMode.ACTIVE ) {

            String localAddr = SocketManagerFactory.getDefaultManager().getLocalAddress();
            logger.debug( "Active data connection. Local address: " + localAddr );            
            
            IFTPServerSocket adc = 
                    SocketManagerFactory.getDefaultManager().createServerSocket( 
                        portRangeBegin, portRangeEnd, localAddr );                                      
                                    
            int port = adc.bind(); // choose a random port in the specified range                        
            
            int hi = (port & 0xff00) >>> 8;
            int lo = port & 0xff;
            
            String bind = new StringBuffer()
                    .append( ((int) bindIp[0]) & 0xff ).append (",")
                    .append( ((int) bindIp[1]) & 0xff ).append (",")
                    .append( ((int) bindIp[2]) & 0xff ).append (",")
                    .append( ((int) bindIp[3]) & 0xff ).append (",")
                    .append( hi ).append (",")
                    .append( lo ).toString ();                                                                        
            
            logger.debug( "Active data connection. Binding: " + bind );            
            
            sendAndValidateCommand( "PORT " + bind, 200 );                                    
            return adc;
            
        }
        else if ( mode == FTPMode.PASSIVE ) {                                    
            
//          227 Entering Passive Mode (193,202,89,250,155,59)            
//            
//          PASV
//                  227
//                  500, 501, 502, 421, 530
                       
            FTPReply rp = sendAndValidateCommand( "PASV", 227 );
            String ms = rp.getText();
            
            int s = ms.indexOf('(');
            int e = ms.indexOf(')');

            // handle another format
            if (s < 0 && e < 0) {
                s = ms.toLowerCase().indexOf("mode") + 4;
                e = ms.length();
            }
            
            String ips = ms.substring(s +1, e);            
            
            int i = ips.lastIndexOf( ',' );
            if ( i == -1  ) {
                throw new InvalidReplyException( "Invalid IP format: " + ms );
            }
            String p1 = ips.substring( i +1 );
            ips = ips.substring( 0, i );
            
            i = ips.lastIndexOf( ',' );
            if ( i == -1  ) {
                throw new InvalidReplyException( "Invalid IP format: " + ms );
            }
            
            String p2 = ips.substring( i +1 );
            ips = ips.substring( 0, i );
            
            ips = ips.replace( ',', '.' );
            
            int port = 0;
            
            try {
                port = ((Integer.parseInt( p2 ) << 8) & 0xff00) | (Integer.parseInt( p1 ) & 0xff );
            }
            catch ( NumberFormatException ne ) {
                throw new InvalidReplyException( "Invalid port format: " + p2 + ", " + p1 );
            }
            
            IFTPClientSocket pdc =
                SocketManagerFactory.getDefaultManager().createClientSocket( ips, port );
            pdc.connect();
            return pdc;
        }
    
        else {
            throw new RuntimeException( "Invalid FTP mode" );
        }
    }

    public void addMessageListener(FTPMessageListener listener) {
        if ( ! msgListeners.contains( listener ) ) {
            msgListeners.addElement( listener );
        }
    }

    public boolean remMessageListener(FTPMessageListener listener) {
        return msgListeners.removeElement( listener );
    }
    
    private void logCommand( String msg ) {        
        if ( msg.toUpperCase().startsWith("PASS ") ) {
            msg = "PASS *****";
        }
        logger.info( "Command: " + msg );
        for (int i = 0; i < msgListeners.size(); i++) {
            ((FTPMessageListener)msgListeners.elementAt( i )).outgoingCommand( msg );
        }
    }
    
    private void logReply( String msg ) {
        logger.info( "Reply: " + msg );
        for (int i = 0; i < msgListeners.size(); i++) {
            ((FTPMessageListener)msgListeners.elementAt( i )).incomingReply( msg );
        }
    }    
    
    public void setTimeout(int timeout) throws FTPSocketException {        
        socket.setTimeout( timeout );
    }

    public String getRemoteHost() {
        return remoteHost;
    }

    public void setRemoteHost(String remoteHost) {
        this.remoteHost = remoteHost;
    }

    public int getRemotePort() {
        return remotePort;
    }

    public void setRemotePort(int remotePort) {
        this.remotePort = remotePort;
    }

    public int[] getBindIp() {
        return bindIp;
    }

    public void setBindIp(int[] bindIp) {
        this.bindIp = bindIp;
    }

    public void setPortRange( int begin, int end ) {
        this.portRangeBegin = begin;
        this.portRangeEnd   = end;
    }
    
    public int[] getPortRange() {
        return new int[]{ portRangeBegin, portRangeEnd };
    }

}
